Auto-detect and surface spawned agent servers#61
Conversation
Design covers detection across Codex, ACP (Claude/Cursor), and local PTY sources via a hybrid argv-hint + stdout-regex + OS socket-probe pipeline. UI surfaces detected servers as a Servers tab inside ThreadTerminalDrawer with xterm.js log tailing, plus a count badge in BranchToolbar.
Adds subscribeDetectedServerEvents (streaming), detectedServers.stop, and detectedServers.openInBrowser to WS_METHODS and WsRpcGroup in packages/contracts/src/rpc.ts, plus stub handlers in apps/server/src/ws.ts to satisfy the exhaustive HandlersFrom type check.
…xpansion Replace loose endsWith check with basename equality so paths like ./node_modules/.bin/vite still match while snextflix/myremix do not, limit positional fallback to the first 3 non-flag tokens, and add comments explaining the one-level recursion guard against infinite loops.
… runtime escape - Split SocketProbe.ts into SocketProbe.ts (tag/types only) and SocketProbeLive.ts (OS-selecting Layer). This breaks the circular dependency where the OS adapter files imported SocketProbe while SocketProbe.ts imported the adapters, causing ReferenceError at test runtime. Update server.ts to import from SocketProbeLive.ts. - Capture the sniffer unsubscribe handle (unsubCandidate) returned by onCandidate() in both trackAgentCommand and trackPty, and call it in end() to prevent callbacks firing after the server has exited. - Replace Effect.runPromise() escape-hatch calls inside sniffer/tracker callbacks with runFork() (Effect.runForkWith(context)), matching the established pattern used in terminal/Layers/Manager.ts for PTY data callbacks.
- Reformat the implementation plan and design spec - Normalize examples and code snippets for readability
📝 WalkthroughWalkthroughEnd-to-end detected-servers feature: adds contracts and WS RPCs, backend pipeline (argv hinting, stdout sniffer, OS socket probes, liveness), in-memory registry and service, ingress trackers for agent/PTY sources, runtime wiring, web UI/store/RPC client, shared LineBuffer, and comprehensive tests/fixtures. ChangesDetected Servers Feature
Estimated code review effort 🎯 5 (Critical) | ⏱️ ~120 minutes
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
Actionable comments posted: 14
🧹 Nitpick comments (7)
apps/server/src/detectedServers/Layers/SocketProbe.Linux.ts (1)
88-111: 💤 Low valueConsider avoiding non-null assertion for clarity.
Line 110 uses a non-null assertion (
!) after checkinginodeToPid.has(r.inode)on line 109. While safe, using a more explicit pattern improves readability and type safety.♻️ Optional refinement
const rows = [...parseProcTcpRows(tcpText), ...parseProcTcp6Rows(tcp6Text)]; - return rows - .filter((r) => r.state === "LISTEN" && inodeToPid.has(r.inode)) - .map((r) => ({ pid: inodeToPid.get(r.inode)!, port: r.port, host: r.host })); + const results: ProbeResult[] = []; + for (const r of rows) { + if (r.state === "LISTEN") { + const pid = inodeToPid.get(r.inode); + if (pid !== undefined) results.push({ pid, port: r.port, host: r.host }); + } + } + return results;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/server/src/detectedServers/Layers/SocketProbe.Linux.ts` around lines 88 - 111, The code in probeImpl uses a non-null assertion inodeToPid.get(r.inode)! after checking inodeToPid.has(r.inode); replace this with an explicit safe lookup to avoid the bang: when building the result from rows, obtain const pid = inodeToPid.get(r.inode) and only return entries when pid is defined (or use a flatMap/filter that discards undefined pids) so the final map uses the real pid variable instead of r.inode!; update references in the rows -> filter/map pipeline (rows, inodeToPid, r.inode) accordingly to preserve types and readability.apps/server/src/detectedServers/Layers/SocketProbe.Darwin.ts (1)
5-29: 💤 Low valueConsider adding a comment for the
parts.length - 1slice.Line 14 slices to
parts.length - 1to exclude the trailing"(LISTEN)"token from the name field. While correct, a brief inline comment would improve readability for future maintainers.📝 Suggested comment
- const nameField = parts.slice(8, parts.length - 1).join(" "); + // Exclude trailing "(LISTEN)" from name field + const nameField = parts.slice(8, parts.length - 1).join(" ");🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/server/src/detectedServers/Layers/SocketProbe.Darwin.ts` around lines 5 - 29, Add a brief inline comment explaining why parts.slice(8, parts.length - 1) excludes the trailing "(LISTEN)" token when building nameField in parseLsofOutput; place the comment near the slice expression (in the parseLsofOutput function) noting that the last token is "(LISTEN)" and should not be included in the name/host:port parsing so future readers understand the -1 adjustment.apps/server/src/detectedServers/StdoutSniffer.test.ts (2)
94-100: ⚡ Quick winAdd length assertion before accessing
out[0].The ANSI stripping test accesses
out[0]!without first asserting the array has at least one element. Addexpect(out).toHaveLength(1);before line 99 for consistency with the chunked URL test above.📋 Proposed fix
it("strips ANSI before matching", () => { const sniffer = new StdoutSniffer(); const out: { url: string }[] = []; sniffer.onCandidate((c) => out.push(c)); sniffer.feed("\x1b[36m ➜ Local:\x1b[0m \x1b[1mhttp://localhost:5173/\x1b[0m\n"); + expect(out).toHaveLength(1); expect(out[0]!.url).toBe("http://localhost:5173/"); });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/server/src/detectedServers/StdoutSniffer.test.ts` around lines 94 - 100, In the "strips ANSI before matching" test for StdoutSniffer, add an assertion that the out array has one element before accessing out[0]; specifically, insert expect(out).toHaveLength(1) immediately after feeding the sniffer and before the existing expect(out[0]!.url).toBe(...) so the test checks presence of the candidate before indexing.
21-82: ⚡ Quick winAdd length assertions before accessing array elements.
Most tests directly access
out[0]!without first asserting the array length. If the sniffer fails to detect a URL, the test will fail with an unclear "Cannot read properties of undefined" error instead of a descriptive assertion failure. The Vite test (line 15) demonstrates the correct pattern withexpect(out).toHaveLength(1)before accessing elements.📋 Proposed fix to add length checks
it("extracts Next URL", () => { const sniffer = new StdoutSniffer(); const out: { url: string; port: number; framework: string }[] = []; sniffer.onCandidate((c) => out.push(c)); sniffer.feed(readFileSync(fixturePath("next"), "utf8")); + expect(out).toHaveLength(1); expect(out[0]!.url).toBe("http://localhost:3000"); expect(out[0]!.framework).toBe("next"); });Apply the same pattern to Nuxt (line 30+), Astro (line 39+), Remix (line 48+), Wrangler (line 57+), Webpack (line 66+), and Express (line 75+) tests.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/server/src/detectedServers/StdoutSniffer.test.ts` around lines 21 - 82, Tests for StdoutSniffer access out[0] without asserting the array length, which can produce unclear errors; before any expect that reads out[0] (in the Nuxt, Astro, Remix, Wrangler, Webpack and Express tests) add an assertion like expect(out).toHaveLength(1) so the test fails with a clear message if no candidate was emitted, then proceed to assert out[0]!.url / .port / .framework as before.apps/server/src/detectedServers/ArgvHinter.test.ts (1)
1-98: ⚡ Quick winAdd test coverage for shortcut invocation (yarn/pnpm/bun).
The code at lines 64-73 in
ArgvHinter.tssupports shortcut invocations likeyarn dev,pnpm dev, andbun dev(without the explicitrunkeyword). This code path is currently untested. Add test cases to verify this behavior.📋 Suggested test cases
it("treats npm run build as build, not server", () => { const got = hintFromArgv(["npm", "run", "build"], { scripts: { build: "vite build" } }); expect(got).toEqual({ framework: "vite", isLikelyServer: false }); }); + it("expands yarn dev shortcut (without explicit run)", () => { + const got = hintFromArgv(["yarn", "dev"], { scripts: { dev: "vite" } }); + expect(got).toEqual({ framework: "vite", isLikelyServer: true }); + }); + + it("expands pnpm start shortcut", () => { + const got = hintFromArgv(["pnpm", "start"], { scripts: { start: "next dev" } }); + expect(got).toEqual({ framework: "next", isLikelyServer: true }); + }); + + it("expands bun serve shortcut", () => { + const got = hintFromArgv(["bun", "serve"], { scripts: { serve: "astro dev" } }); + expect(got).toEqual({ framework: "astro", isLikelyServer: true }); + }); // Regression tests: prefix-match false positives🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/server/src/detectedServers/ArgvHinter.test.ts` around lines 1 - 98, Add tests covering shortcut invocations (yarn/pnpm/bun) to exercise hintFromArgv's branch for commands like "yarn dev" (no "run"); specifically add cases that call hintFromArgv(["yarn","dev"], {scripts:{dev:"vite"}}), hintFromArgv(["pnpm","dev"], {scripts:{dev:"vite"}}) and hintFromArgv(["bun","dev"], {scripts:{dev:"vite"}}) and assert they return { framework: "vite", isLikelyServer: true }, plus at least one build shortcut test (e.g. ["yarn","build"] with scripts.build "vite build") asserting { framework: "vite", isLikelyServer: false }; reference hintFromArgv in ArgvHinter.test.ts and add the new it(...) blocks alongside the existing tests.apps/web/src/components/BranchToolbar.tsx (1)
134-134: ⚡ Quick winAvoid creating a new function on every render.
The pattern
onClick={onOpenServersTab ?? (() => {})}creates a new no-op function each timeBranchToolbarre-renders whenonOpenServersTabis undefined. IfDetectedServersBadgeis memoized, this will cause unnecessary child re-renders.Suggested refactor
Option 1: Use a stable NOOP constant
+const NOOP = () => {}; + export const BranchToolbar = memo(function BranchToolbar({ ... }: BranchToolbarProps) { ... - <DetectedServersBadge servers={detectedServers} onClick={onOpenServersTab ?? (() => {})} /> + <DetectedServersBadge servers={detectedServers} onClick={onOpenServersTab ?? NOOP} />Option 2: Make onClick optional in DetectedServersBadge and conditionally render the badge
- <DetectedServersBadge servers={detectedServers} onClick={onOpenServersTab ?? (() => {})} /> + {onOpenServersTab && <DetectedServersBadge servers={detectedServers} onClick={onOpenServersTab} />}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/components/BranchToolbar.tsx` at line 134, BranchToolbar currently passes onClick={onOpenServersTab ?? (() => {})}, which allocates a new noop on every render and can break DetectedServersBadge memoization; fix by using a stable NOOP constant (e.g., define a module-level NOOP and pass that instead) or make DetectedServersBadge's onClick prop optional and only pass onOpenServersTab when it's defined (or conditionally render the badge), referencing DetectedServersBadge and the onOpenServersTab prop in BranchToolbar to locate the change.apps/web/src/terminalStateStore.ts (1)
758-855: ⚡ Quick winKeep
terminalDrawerKindByThreadKeyin sync with terminal state cleanupLine 758 onward clears/removes thread-scoped terminal state, launch context, and event buffers, but leaves
terminalDrawerKindByThreadKeyuntouched. This creates stale per-thread UI state and unbounded growth over time.Suggested fix
clearTerminalState: (threadRef) => set((state) => { const threadKey = terminalThreadKey(threadRef); @@ const { [threadKey]: _removed, ...remainingLaunchContexts } = state.terminalLaunchContextByThreadKey; + const { [threadKey]: _removedDrawerKind, ...remainingDrawerKinds } = + state.terminalDrawerKindByThreadKey; @@ return { terminalStateByThreadKey: nextTerminalStateByThreadKey, terminalLaunchContextByThreadKey: remainingLaunchContexts, + terminalDrawerKindByThreadKey: remainingDrawerKinds, terminalEventEntriesByKey: nextTerminalEventEntriesByKey, }; }), removeTerminalState: (threadRef) => set((state) => { @@ const nextLaunchContexts = { ...state.terminalLaunchContextByThreadKey }; delete nextLaunchContexts[threadKey]; + const nextDrawerKinds = { ...state.terminalDrawerKindByThreadKey }; + delete nextDrawerKinds[threadKey]; return { terminalStateByThreadKey: nextTerminalStateByThreadKey, terminalLaunchContextByThreadKey: nextLaunchContexts, + terminalDrawerKindByThreadKey: nextDrawerKinds, terminalEventEntriesByKey: nextTerminalEventEntriesByKey, }; }), removeOrphanedTerminalStates: (activeThreadKeys) => set((state) => { @@ const orphanedLaunchContextIds = Object.keys( state.terminalLaunchContextByThreadKey, ).filter((key) => !activeThreadKeys.has(key)); + const orphanedDrawerKindIds = Object.keys( + state.terminalDrawerKindByThreadKey, + ).filter((key) => !activeThreadKeys.has(key)); @@ if ( orphanedIds.length === 0 && orphanedLaunchContextIds.length === 0 && + orphanedDrawerKindIds.length === 0 && !removedEventEntries ) { return state; } @@ const nextLaunchContexts = { ...state.terminalLaunchContextByThreadKey }; for (const id of orphanedLaunchContextIds) { delete nextLaunchContexts[id]; } + const nextDrawerKinds = { ...state.terminalDrawerKindByThreadKey }; + for (const id of orphanedDrawerKindIds) { + delete nextDrawerKinds[id]; + } return { terminalStateByThreadKey: next, terminalLaunchContextByThreadKey: nextLaunchContexts, + terminalDrawerKindByThreadKey: nextDrawerKinds, terminalEventEntriesByKey: nextTerminalEventEntriesByKey, }; }),🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/terminalStateStore.ts` around lines 758 - 855, The cleanup functions clearTerminalState, removeTerminalState, and removeOrphanedTerminalStates must also remove per-thread entries from terminalDrawerKindByThreadKey to avoid stale UI state: for clearTerminalState and removeTerminalState compute whether state.terminalDrawerKindByThreadKey[threadKey] existed (hadDrawer) and create a shallow copy nextDrawerKinds from state.terminalDrawerKindByThreadKey, delete nextDrawerKinds[threadKey], include nextDrawerKinds in the returned object, and include hadDrawer in the early-return condition; for removeOrphanedTerminalStates compute orphaned drawer keys from Object.keys(state.terminalDrawerKindByThreadKey) that are not in activeThreadKeys, delete them from a copy nextDrawerKinds, include nextDrawerKinds in the returned state, and include their presence in the early-return check.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/server/integration/detectedServersCodex.integration.test.ts`:
- Around line 42-45: Replace the fixed 100ms sleep by polling the registry until
the expected candidate appears or a timeout expires: repeatedly call
registry.getCurrent("thread-1") (using the existing registry.getCurrent symbol)
in a short sleep loop (instead of Effect.sleep(Duration.millis(100)) once) and
break as soon as current.length === 1, failing the test if the timeout is
reached; use Effect.sleep with a small interval and a reasonable overall timeout
to keep assertions deterministic in CI.
In `@apps/server/src/detectedServers/Layers/DetectedServersIngress.ts`:
- Around line 11-17: The argvHasInspect function currently only captures inspect
flags that include an explicit "=port" and therefore ignores bare flags like
"--inspect", "--inspect-brk", and "--inspect-wait" which default to port 9229;
update argvHasInspect to also detect those bare forms and push 9229 when matched
(either by extending the regex to allow an optional "=...(\d+)" capture and
defaulting to 9229 when no capture is present, or by adding an additional test
for /^\-\-inspect(?:-brk|-wait)?$/ that pushes 9229) so that all inspect forms
are included in the returned ports array.
- Around line 212-224: The probe loop in DetectedServersIngress is repeatedly
calling registry.registerOrUpdate with status: "confirmed" (see
registry.registerOrUpdate and the liveSeenAt sleep logic), causing duplicate
unchanged updates; change the logic to suppress re-emitting identical
"confirmed" patches unless something meaningful changed (status, port, host) or
a configured debounce/heartbeat interval has elapsed — e.g., read the current
registry entry (or track lastEmittedStatus/lastEmittedAt for the
identityKey/threadId) and only call registry.registerOrUpdate when the
status/patch differs or when enough time has passed to justify a refresh; keep
references to identityKey, source.threadId, matching.port/host and liveSeenAt in
the condition so existing behavior remains for real changes or periodic
heartbeats.
In `@apps/server/src/detectedServers/Layers/Registry.ts`:
- Around line 180-184: The publish method currently iterates listeners and a
thrown exception from one listener aborts delivery; update publish(threadId:
string, event: DetectedServerEvent) to wrap each listener invocation (for const
l of set) in a try/catch so a failing listener does not stop others and does not
propagate to the caller; inside the catch, log the error with contextual info
(e.g., include threadId and the listener identity) using the available logger
(or console.error if no logger exists) and continue delivering to remaining
listeners.
- Around line 61-72: getCurrent and findById expose live DetectedServer objects
from the internal byThread maps, allowing external callers to mutate registry
state and bypass registerOrUpdate checks; change both to return defensive copies
(e.g., map each DetectedServer to a shallow clone or Object.assign({}, server)
or JSON-deep-clone as appropriate for your fields) so callers get copies not
references, and keep registerOrUpdate operating on the original objects stored
in byThread; update getCurrent to return an array of cloned DetectedServer
objects and update findById to return a cloned DetectedServer or undefined.
In `@apps/server/src/provider/Layers/CodexSessionRuntime.ts`:
- Around line 800-814: Existing logic finalizes trackers only on
"item/completed", leaving trackers in trackerMap running if the session is
closed or interrupted; update the session shutdown/interrupt handler(s) in
CodexSessionRuntime to iterate over trackerMap, call tracker.end(...) with a
non-success terminal status (e.g., "error" or "cancel") for each tracker, and
then delete or clear those entries so no stale detection work remains; ensure
the same cleanup is also invoked from any early-exit/exception path so trackers
created in the "item/commandExecution/outputDelta" / commandExecution flow are
always finalized.
In `@apps/server/src/provider/Layers/CursorAdapter.ts`:
- Around line 777-806: The trackerMap is currently keyed only by toolCallId
which can collide across threads and the branch that creates a tracker when the
first event is terminal (status "completed" or "failed") never ends it; fix by
using a thread-scoped key (e.g., combine ctx.threadId and toolCallId) when
reading/writing trackerMap entries and, inside the block that creates a new
tracker via detectedServers.trackAgentCommand, check the incoming toolStatus: if
it is "completed" or "failed" immediately call tracker.end(...) with "success"
or "error" respectively and remove the tracker from trackerMap; keep feeding
detail when present and ensure trackerMap.delete is called for terminal cases.
In `@apps/server/src/ws.ts`:
- Around line 2046-2053: The subscription callback currently calls
Effect.runSync(Queue.offer(queue, event)) which blocks and can throw; change it
to run the offer effect non-blockingly by either invoking Effect.runCallback(()
=> Queue.offer(queue, event)) or by forking the effect
(Effect.fork(Queue.offer(queue, event))) so exceptions are handled inside the
effect runtime; locate the callback passed to detectedServerRegistry.subscribe
inside the liveStream creation and replace Effect.runSync with one of these
non-blocking patterns (or have the subscribe callback return the Effect directly
if the subscribe API supports effectful callbacks) to match the safer pattern
used elsewhere.
In `@apps/web/src/components/ChatView.tsx`:
- Around line 1059-1067: Effect currently reads a non-reactive snapshot via
readEnvironmentConnection inside useEffect so if it returns null initially the
subscription never retries; change the effect to derive a reactive connection
value (e.g. call readEnvironmentConnection(environmentId) at render time or
introduce a useEnvironmentConnection(environmentId) hook) and include that
connection in the useEffect deps, then only attach detectedServers.onEvent when
connection is non-null and ensure you clean up on return; this affects the
useEffect block that references readEnvironmentConnection, scopeThreadRef,
scopedThreadKey, detectedServers.onEvent and
useDetectedServerStore.getState().handleEvent so that subscriptions retry
automatically when the environment connection becomes available.
In `@apps/web/src/components/detectedServers/DetectedServerRow.tsx`:
- Around line 25-83: The row currently uses a outer <button> (wrapping the
entire row) while also rendering child <button>s (onOpen, onCopy, onStop), which
creates nested interactive elements; change the outer element in
DetectedServerRow to a non-interactive container (e.g., <div>) with
role="button", tabIndex={0}, the same className logic, onClick={onSelect} and an
onKeyDown handler that triggers onSelect for Enter/Space so keyboard users still
activate the row; keep the inner buttons as real <button>s, preserve
e.stopPropagation() in their onClick handlers, and ensure the aria-labels and
disabled logic for onStop remain unchanged so accessibility and behavior are
preserved.
In `@apps/web/src/components/detectedServers/DetectedServersPanel.tsx`:
- Around line 30-49: Wrap the external async calls in handleStop, handleOpen and
handleCopy with try/catch so RPC/clipboard failures are handled: in handleStop
and handleOpen, await the RPC call inside a try block and catch errors to log
them (process via console.error or a UI error toast) and optionally show a
user-friendly message; in handleCopy, await navigator.clipboard.writeText(url)
inside try/catch and handle permission/clipboard errors similarly; ensure you
return early if prerequisites (threadRef/connection) are missing, and avoid
unhandled promise rejections by not using fire-and-forget (remove the void
usage).
In `@docs/superpowers/specs/2026-05-13-auto-detect-agent-servers-design.md`:
- Line 488: The file-level summary references the wrong WS handler path
(apps/server/src/wsServer.ts) while the PR stack context and RPC handlers live
under apps/server/src/ws.ts; update the summary to match the actual file name
used by the RPC handlers (change apps/server/src/wsServer.ts →
apps/server/src/ws.ts) or, if the canonical file is wsServer.ts, update the
stack/context references to that name so both the summary and the RPC handler
context (ws.ts / wsServer.ts) are consistent and navigable.
- Line 71: Several fenced code blocks in the design doc are missing language
identifiers causing MD040 lint failures; update the fences at the listed
locations (Line 71, 94, 126, 168, 180, 248, 355, 470) to include the appropriate
language tags as shown in the suggested patch (e.g., use ```text for directory
listings and UI diagrams, ```ts for TypeScript snippets like
DetectedServerEvent, ServerStatus and the serversByThreadKey record, and
```regex for the URL regex), and ensure each closing fence remains ``` so the
blocks are properly recognized by the linter.
In `@packages/contracts/src/detectedServers.ts`:
- Around line 71-82: The patch schema for updates currently allows non-positive
port and pid values; update the patch Schema.Struct (the "patch" property in the
UpdatedEvent) to apply the same isGreaterThan(0) constraint used in the base
DetectedServer schema for the port and pid fields so patched values cannot be
zero or negative (match the validation on DetectedServer.port and
DetectedServer.pid).
---
Nitpick comments:
In `@apps/server/src/detectedServers/ArgvHinter.test.ts`:
- Around line 1-98: Add tests covering shortcut invocations (yarn/pnpm/bun) to
exercise hintFromArgv's branch for commands like "yarn dev" (no "run");
specifically add cases that call hintFromArgv(["yarn","dev"],
{scripts:{dev:"vite"}}), hintFromArgv(["pnpm","dev"], {scripts:{dev:"vite"}})
and hintFromArgv(["bun","dev"], {scripts:{dev:"vite"}}) and assert they return {
framework: "vite", isLikelyServer: true }, plus at least one build shortcut test
(e.g. ["yarn","build"] with scripts.build "vite build") asserting { framework:
"vite", isLikelyServer: false }; reference hintFromArgv in ArgvHinter.test.ts
and add the new it(...) blocks alongside the existing tests.
In `@apps/server/src/detectedServers/Layers/SocketProbe.Darwin.ts`:
- Around line 5-29: Add a brief inline comment explaining why parts.slice(8,
parts.length - 1) excludes the trailing "(LISTEN)" token when building nameField
in parseLsofOutput; place the comment near the slice expression (in the
parseLsofOutput function) noting that the last token is "(LISTEN)" and should
not be included in the name/host:port parsing so future readers understand the
-1 adjustment.
In `@apps/server/src/detectedServers/Layers/SocketProbe.Linux.ts`:
- Around line 88-111: The code in probeImpl uses a non-null assertion
inodeToPid.get(r.inode)! after checking inodeToPid.has(r.inode); replace this
with an explicit safe lookup to avoid the bang: when building the result from
rows, obtain const pid = inodeToPid.get(r.inode) and only return entries when
pid is defined (or use a flatMap/filter that discards undefined pids) so the
final map uses the real pid variable instead of r.inode!; update references in
the rows -> filter/map pipeline (rows, inodeToPid, r.inode) accordingly to
preserve types and readability.
In `@apps/server/src/detectedServers/StdoutSniffer.test.ts`:
- Around line 94-100: In the "strips ANSI before matching" test for
StdoutSniffer, add an assertion that the out array has one element before
accessing out[0]; specifically, insert expect(out).toHaveLength(1) immediately
after feeding the sniffer and before the existing expect(out[0]!.url).toBe(...)
so the test checks presence of the candidate before indexing.
- Around line 21-82: Tests for StdoutSniffer access out[0] without asserting the
array length, which can produce unclear errors; before any expect that reads
out[0] (in the Nuxt, Astro, Remix, Wrangler, Webpack and Express tests) add an
assertion like expect(out).toHaveLength(1) so the test fails with a clear
message if no candidate was emitted, then proceed to assert out[0]!.url / .port
/ .framework as before.
In `@apps/web/src/components/BranchToolbar.tsx`:
- Line 134: BranchToolbar currently passes onClick={onOpenServersTab ?? (() =>
{})}, which allocates a new noop on every render and can break
DetectedServersBadge memoization; fix by using a stable NOOP constant (e.g.,
define a module-level NOOP and pass that instead) or make DetectedServersBadge's
onClick prop optional and only pass onOpenServersTab when it's defined (or
conditionally render the badge), referencing DetectedServersBadge and the
onOpenServersTab prop in BranchToolbar to locate the change.
In `@apps/web/src/terminalStateStore.ts`:
- Around line 758-855: The cleanup functions clearTerminalState,
removeTerminalState, and removeOrphanedTerminalStates must also remove
per-thread entries from terminalDrawerKindByThreadKey to avoid stale UI state:
for clearTerminalState and removeTerminalState compute whether
state.terminalDrawerKindByThreadKey[threadKey] existed (hadDrawer) and create a
shallow copy nextDrawerKinds from state.terminalDrawerKindByThreadKey, delete
nextDrawerKinds[threadKey], include nextDrawerKinds in the returned object, and
include hadDrawer in the early-return condition; for
removeOrphanedTerminalStates compute orphaned drawer keys from
Object.keys(state.terminalDrawerKindByThreadKey) that are not in
activeThreadKeys, delete them from a copy nextDrawerKinds, include
nextDrawerKinds in the returned state, and include their presence in the
early-return check.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 7b560b23-e3e1-4390-aaa6-096a979172ad
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (65)
apps/server/integration/OrchestrationEngineHarness.integration.tsapps/server/integration/detectedServersCodex.integration.test.tsapps/server/integration/detectedServersPty.integration.test.tsapps/server/package.jsonapps/server/src/detectedServers/ArgvHinter.test.tsapps/server/src/detectedServers/Layers/ArgvHinter.tsapps/server/src/detectedServers/Layers/DetectedServersIngress.tsapps/server/src/detectedServers/Layers/LivenessHeartbeat.tsapps/server/src/detectedServers/Layers/Registry.tsapps/server/src/detectedServers/Layers/SocketProbe.Darwin.tsapps/server/src/detectedServers/Layers/SocketProbe.Linux.tsapps/server/src/detectedServers/Layers/SocketProbe.Windows.tsapps/server/src/detectedServers/Layers/SocketProbe.tsapps/server/src/detectedServers/Layers/SocketProbeLive.tsapps/server/src/detectedServers/Layers/StdoutSniffer.tsapps/server/src/detectedServers/Registry.test.tsapps/server/src/detectedServers/Services/DetectedServerRegistry.tsapps/server/src/detectedServers/SocketProbe.Darwin.test.tsapps/server/src/detectedServers/SocketProbe.Linux.test.tsapps/server/src/detectedServers/SocketProbe.Windows.test.tsapps/server/src/detectedServers/StdoutSniffer.test.tsapps/server/src/detectedServers/__fixtures__/proc/tcp.txtapps/server/src/detectedServers/__fixtures__/proc/tcp6.txtapps/server/src/detectedServers/__fixtures__/stdout/astro.txtapps/server/src/detectedServers/__fixtures__/stdout/express.txtapps/server/src/detectedServers/__fixtures__/stdout/next.txtapps/server/src/detectedServers/__fixtures__/stdout/nuxt.txtapps/server/src/detectedServers/__fixtures__/stdout/remix.txtapps/server/src/detectedServers/__fixtures__/stdout/vite.txtapps/server/src/detectedServers/__fixtures__/stdout/webpack.txtapps/server/src/detectedServers/__fixtures__/stdout/wrangler.txtapps/server/src/provider/Drivers/CodexDriver.tsapps/server/src/provider/Drivers/CursorDriver.tsapps/server/src/provider/Layers/CodexAdapter.test.tsapps/server/src/provider/Layers/CodexAdapter.tsapps/server/src/provider/Layers/CodexSessionRuntime.tsapps/server/src/provider/Layers/CursorAdapter.test.tsapps/server/src/provider/Layers/CursorAdapter.tsapps/server/src/provider/Layers/ProviderInstanceRegistryLive.test.tsapps/server/src/provider/Layers/ProviderRegistry.test.tsapps/server/src/server.test.tsapps/server/src/server.tsapps/server/src/terminal/Layers/Manager.test.tsapps/server/src/terminal/Layers/Manager.tsapps/server/src/ws.tsapps/web/src/components/BranchToolbar.tsxapps/web/src/components/BranchToolbar/DetectedServersBadge.test.tsxapps/web/src/components/BranchToolbar/DetectedServersBadge.tsxapps/web/src/components/ChatView.tsxapps/web/src/components/ThreadTerminalDrawer.tsxapps/web/src/components/detectedServers/DetectedServerLogView.tsxapps/web/src/components/detectedServers/DetectedServerRow.tsxapps/web/src/components/detectedServers/DetectedServersPanel.tsxapps/web/src/detectedServerStore.test.tsapps/web/src/detectedServerStore.tsapps/web/src/rpc/wsRpcClient.tsapps/web/src/terminalStateStore.tsdocs/superpowers/plans/2026-05-13-auto-detect-agent-servers.mddocs/superpowers/specs/2026-05-13-auto-detect-agent-servers-design.mdpackages/contracts/src/detectedServers.tspackages/contracts/src/index.tspackages/contracts/src/rpc.tspackages/shared/package.jsonpackages/shared/src/lineBuffer.test.tspackages/shared/src/lineBuffer.ts
| <button | ||
| type="button" | ||
| onClick={onSelect} | ||
| className={cn( | ||
| "group flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-xs hover:bg-accent", | ||
| active && "bg-accent", | ||
| )} | ||
| > | ||
| <Server className="h-3.5 w-3.5 shrink-0 text-muted-foreground" /> | ||
| <div className="flex min-w-0 flex-1 flex-col"> | ||
| <div className="flex items-center gap-1.5"> | ||
| <span className="font-medium">{server.framework}</span> | ||
| <span className={cn("rounded px-1.5 py-0.5 text-[10px]", STATUS_PILL_CLASS[server.status])}> | ||
| {server.status} | ||
| </span> | ||
| </div> | ||
| <div className="truncate text-muted-foreground">{server.url ?? "—"}</div> | ||
| </div> | ||
| <div className="flex shrink-0 items-center gap-0.5 opacity-0 group-hover:opacity-100"> | ||
| {server.url && ( | ||
| <button | ||
| type="button" | ||
| aria-label="Open in browser" | ||
| className="rounded p-1 hover:bg-background" | ||
| onClick={(e) => { | ||
| e.stopPropagation(); | ||
| onOpen(); | ||
| }} | ||
| > | ||
| <ExternalLink className="h-3.5 w-3.5" /> | ||
| </button> | ||
| )} | ||
| {server.url && ( | ||
| <button | ||
| type="button" | ||
| aria-label="Copy URL" | ||
| className="rounded p-1 hover:bg-background" | ||
| onClick={(e) => { | ||
| e.stopPropagation(); | ||
| onCopy(); | ||
| }} | ||
| > | ||
| <Copy className="h-3.5 w-3.5" /> | ||
| </button> | ||
| )} | ||
| <button | ||
| type="button" | ||
| aria-label="Stop" | ||
| disabled={server.status === "exited" || server.status === "crashed"} | ||
| className="rounded p-1 hover:bg-background disabled:opacity-30" | ||
| onClick={(e) => { | ||
| e.stopPropagation(); | ||
| onStop(); | ||
| }} | ||
| > | ||
| <Square className="h-3.5 w-3.5" /> | ||
| </button> | ||
| </div> | ||
| </button> |
There was a problem hiding this comment.
Avoid nested <button> elements in server rows
Line 25 wraps the whole row in a <button>, then Lines 45/58/70 add child <button> controls. Nested interactive elements are invalid and can break keyboard and click behavior.
Suggested fix
-export const DetectedServerRow = ({ server, active, onSelect, onOpen, onCopy, onStop }: Props) => (
- <button
- type="button"
- onClick={onSelect}
+export const DetectedServerRow = ({ server, active, onSelect, onOpen, onCopy, onStop }: Props) => (
+ <div
+ role="button"
+ tabIndex={0}
+ onClick={onSelect}
+ onKeyDown={(e) => {
+ if (e.key === "Enter" || e.key === " ") {
+ e.preventDefault();
+ onSelect();
+ }
+ }}
className={cn(
"group flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-xs hover:bg-accent",
active && "bg-accent",
)}
>
@@
- </button>
+ </div>
);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web/src/components/detectedServers/DetectedServerRow.tsx` around lines
25 - 83, The row currently uses a outer <button> (wrapping the entire row) while
also rendering child <button>s (onOpen, onCopy, onStop), which creates nested
interactive elements; change the outer element in DetectedServerRow to a
non-interactive container (e.g., <div>) with role="button", tabIndex={0}, the
same className logic, onClick={onSelect} and an onKeyDown handler that triggers
onSelect for Enter/Space so keyboard users still activate the row; keep the
inner buttons as real <button>s, preserve e.stopPropagation() in their onClick
handlers, and ensure the aria-labels and disabled logic for onStop remain
unchanged so accessibility and behavior are preserved.
| const handleStop = async (serverId: string) => { | ||
| if (!threadRef) return; | ||
| const connection = readEnvironmentConnection(threadRef.environmentId); | ||
| if (!connection) return; | ||
| const result = await connection.client.detectedServers.stop({ serverId }); | ||
| if (result.kind === "not-stoppable") { | ||
| console.info("Server managed by agent — interrupt the turn to stop it"); | ||
| } | ||
| }; | ||
|
|
||
| const handleCopy = (url: string) => { | ||
| void navigator.clipboard.writeText(url); | ||
| }; | ||
|
|
||
| const handleOpen = async (serverId: string) => { | ||
| if (!threadRef) return; | ||
| const connection = readEnvironmentConnection(threadRef.environmentId); | ||
| if (!connection) return; | ||
| await connection.client.detectedServers.openInBrowser({ serverId }); | ||
| }; |
There was a problem hiding this comment.
Handle RPC/clipboard failures in action handlers
Lines 30–49 issue external async calls (stop, openInBrowser, clipboard write) without try/catch. In this fire-and-forget wiring, failures become unhandled and users get no feedback.
Suggested fix
const handleStop = async (serverId: string) => {
if (!threadRef) return;
const connection = readEnvironmentConnection(threadRef.environmentId);
if (!connection) return;
- const result = await connection.client.detectedServers.stop({ serverId });
- if (result.kind === "not-stoppable") {
- console.info("Server managed by agent — interrupt the turn to stop it");
+ try {
+ const result = await connection.client.detectedServers.stop({ serverId });
+ if (result.kind === "not-stoppable") {
+ console.info("Server managed by agent — interrupt the turn to stop it");
+ }
+ } catch {
+ console.error("Failed to stop detected server");
}
};
const handleCopy = (url: string) => {
- void navigator.clipboard.writeText(url);
+ void navigator.clipboard.writeText(url).catch(() => {
+ console.error("Failed to copy server URL");
+ });
};
const handleOpen = async (serverId: string) => {
if (!threadRef) return;
const connection = readEnvironmentConnection(threadRef.environmentId);
if (!connection) return;
- await connection.client.detectedServers.openInBrowser({ serverId });
+ try {
+ await connection.client.detectedServers.openInBrowser({ serverId });
+ } catch {
+ console.error("Failed to open detected server in browser");
+ }
};🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web/src/components/detectedServers/DetectedServersPanel.tsx` around
lines 30 - 49, Wrap the external async calls in handleStop, handleOpen and
handleCopy with try/catch so RPC/clipboard failures are handled: in handleStop
and handleOpen, await the RPC call inside a try block and catch errors to log
them (process via console.error or a UI error toast) and optionally show a
user-friendly message; in handleCopy, await navigator.clipboard.writeText(url)
inside try/catch and handle permission/clipboard errors similarly; ensure you
return early if prerequisites (threadRef/connection) are missing, and avoid
unhandled promise rejections by not using fire-and-forget (remove the void
usage).
|
|
||
| ### Module layout | ||
|
|
||
| ``` |
There was a problem hiding this comment.
Add language identifiers to fenced code blocks to satisfy markdown lint.
The fences at Line 71, Line 94, Line 126, Line 168, Line 180, Line 248, Line 355, and Line 470 are missing language tags (MD040), which is a CI/lint issue for docs quality gates.
🛠️ Suggested patch
-```
+```text
apps/server/src/detectedServers/
Services/
...
-```
+```
-```
+```text
Agent tool call (Codex/ACP) Local PTY (terminal/OpenCode)
...
-```
+```
-```
+```text
predicted → candidate → confirmed → live → (restarting → live | exited | crashed)
...
-```
+```
-```
+```ts
DetectedServerEvent =
| { type: "registered"; server: DetectedServer }
...
-```
+```
-```
+```ts
ServerStatus = "predicted" | "candidate" | "confirmed" | "live"
...
-```
+```
-```
+```regex
\bhttps?://(?:localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1?\])(?::\d+)?(?:/\S*)?\b
-```
+```
-```
+```ts
{
serversByThreadKey: Record<threadKey, Map<serverId, DetectedServer>>
...
-```
+```
-```
+```text
apps/server/src/detectedServers/
Services/DetectedServerRegistry.ts (new)
...
-```
+```Also applies to: 94-94, 126-126, 168-168, 180-180, 248-248, 355-355, 470-470
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 71-71: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/superpowers/specs/2026-05-13-auto-detect-agent-servers-design.md` at
line 71, Several fenced code blocks in the design doc are missing language
identifiers causing MD040 lint failures; update the fences at the listed
locations (Line 71, 94, 126, 168, 180, 248, 355, 470) to include the appropriate
language tags as shown in the suggested patch (e.g., use ```text for directory
listings and UI diagrams, ```ts for TypeScript snippets like
DetectedServerEvent, ServerStatus and the serversByThreadKey record, and
```regex for the URL regex), and ensure each closing fence remains ``` so the
blocks are properly recognized by the linter.
|
|
||
| apps/server/src/ | ||
| serverLayers.ts (modified: + ingress) | ||
| wsServer.ts (modified: + RPC handlers) |
There was a problem hiding this comment.
Verify WS handler filename in the file-level summary.
Line 488 lists apps/server/src/wsServer.ts, but the stack context for this PR layer references apps/server/src/ws.ts for WebSocket RPC handlers. Please align this path to avoid navigation confusion.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/superpowers/specs/2026-05-13-auto-detect-agent-servers-design.md` at
line 488, The file-level summary references the wrong WS handler path
(apps/server/src/wsServer.ts) while the PR stack context and RPC handlers live
under apps/server/src/ws.ts; update the summary to match the actual file name
used by the RPC handlers (change apps/server/src/wsServer.ts →
apps/server/src/ws.ts) or, if the canonical file is wsServer.ts, update the
stack/context references to that name so both the summary and the RPC handler
context (ws.ts / wsServer.ts) are consistent and navigable.
| patch: Schema.Struct({ | ||
| status: Schema.optional(ServerStatus), | ||
| framework: Schema.optional(ServerFramework), | ||
| url: Schema.optional(Schema.String), | ||
| port: Schema.optional(Schema.Int), | ||
| host: Schema.optional(Schema.String), | ||
| pid: Schema.optional(Schema.Int), | ||
| liveAt: Schema.optional(Schema.DateTimeUtc), | ||
| lastSeenAt: Schema.optional(Schema.DateTimeUtc), | ||
| exitedAt: Schema.optional(Schema.DateTimeUtc), | ||
| exitReason: Schema.optional(ExitReason), | ||
| }), |
There was a problem hiding this comment.
Inconsistent validation constraints in patch fields.
The port and pid fields in the UpdatedEvent patch lack the isGreaterThan(0) constraint present in the base DetectedServer schema (lines 43, 45). This allows patches with zero or negative values that would violate the main schema's invariants.
🛡️ Proposed fix to add matching constraints
patch: Schema.Struct({
status: Schema.optional(ServerStatus),
framework: Schema.optional(ServerFramework),
url: Schema.optional(Schema.String),
- port: Schema.optional(Schema.Int),
+ port: Schema.optional(Schema.Int.check(Schema.isGreaterThan(0))),
host: Schema.optional(Schema.String),
- pid: Schema.optional(Schema.Int),
+ pid: Schema.optional(Schema.Int.check(Schema.isGreaterThan(0))),
liveAt: Schema.optional(Schema.DateTimeUtc),
lastSeenAt: Schema.optional(Schema.DateTimeUtc),
exitedAt: Schema.optional(Schema.DateTimeUtc),
exitReason: Schema.optional(ExitReason),
}),📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| patch: Schema.Struct({ | |
| status: Schema.optional(ServerStatus), | |
| framework: Schema.optional(ServerFramework), | |
| url: Schema.optional(Schema.String), | |
| port: Schema.optional(Schema.Int), | |
| host: Schema.optional(Schema.String), | |
| pid: Schema.optional(Schema.Int), | |
| liveAt: Schema.optional(Schema.DateTimeUtc), | |
| lastSeenAt: Schema.optional(Schema.DateTimeUtc), | |
| exitedAt: Schema.optional(Schema.DateTimeUtc), | |
| exitReason: Schema.optional(ExitReason), | |
| }), | |
| patch: Schema.Struct({ | |
| status: Schema.optional(ServerStatus), | |
| framework: Schema.optional(ServerFramework), | |
| url: Schema.optional(Schema.String), | |
| port: Schema.optional(Schema.Int.check(Schema.isGreaterThan(0))), | |
| host: Schema.optional(Schema.String), | |
| pid: Schema.optional(Schema.Int.check(Schema.isGreaterThan(0))), | |
| liveAt: Schema.optional(Schema.DateTimeUtc), | |
| lastSeenAt: Schema.optional(Schema.DateTimeUtc), | |
| exitedAt: Schema.optional(Schema.DateTimeUtc), | |
| exitReason: Schema.optional(ExitReason), | |
| }), |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/contracts/src/detectedServers.ts` around lines 71 - 82, The patch
schema for updates currently allows non-positive port and pid values; update the
patch Schema.Struct (the "patch" property in the UpdatedEvent) to apply the same
isGreaterThan(0) constraint used in the base DetectedServer schema for the port
and pid fields so patched values cannot be zero or negative (match the
validation on DetectedServer.port and DetectedServer.pid).
Adds a terminalId field to DetectedServer so the registry knows which TerminalManager session owns a PTY-sourced server. Manager.ts now passes session.terminalId when calling trackPty, and the WS detectedServers.stop handler closes the owning terminal session for source="pty" servers instead of always returning not-stoppable.
Local PTYs run a shell, which ArgvHinter rightly classifies as not a server, so v1 PTY detection was inert. This wires user input through a small line buffer that tokenizes complete lines (Enter/CR) and forwards them via a new PtyTracker.feedCommand. Each likely-server command spawns its own sub-tracker (sniffer + probe fiber + registry entry), so a single PTY can host multiple detected servers in sequence. The line buffer handles backspace and Ctrl-C; pathological in-line editing (cursor keys etc.) leaks bracket characters into the buffer but ArgvHinter classifies them as unknown and skips registration, which is acceptable for v1.
ACP sends `toolCall.detail` cumulatively — every ToolCallUpdated event REPLACES the previous text with a longer version. Forwarding the full detail to StdoutSniffer on every update made it re-parse lines we already saw, emitting duplicate registry events. Extracts the dedup as AcpDetailSuffixDedup so we can unit-test the suffix-extraction policy in isolation, and wires it into the Cursor ACP adapter to feed only the new suffix per toolCallId. Defensive guard for the (unexpected) case where detail shrinks: reset the per-key length and re-feed the full string.
…unSync Replaces global Error throws inside Effect.tryPromise catch callbacks with small Schema-tagged error classes per module (HeartbeatError, PidtreeError, SocketProbeError). These errors are immediately swallowed by orElseSucceed, so the change is cosmetic — it removes the TS36 globalErrorInEffectCatch advisories. Replaces the bare Effect.runSync inside the subscribeDetectedServerEvents Stream.callback handler with Effect.runSyncWith bound to the captured context, matching the canonical pattern used elsewhere for callback->Queue bridging. Removes the TS32 runEffectInsideEffect advisory in ws.ts.
Server tests: - LivenessHeartbeat.test.ts: stubs global fetch and verifies 200/500 → true, AbortError + generic network errors → false. - DetectedServersIngress.test.ts: drives trackAgentCommand and trackPty against stubbed SocketProbe / LivenessHeartbeat layers; covers noop tracker for non-server commands, candidate emission from sniffed URLs, the live promotion path, and that end() interrupts the probe fiber so no further SocketProbe calls fire. - Handlers.test.ts: extracts the WS detectedServers.stop / detectedServers.openInBrowser handler logic into Handlers.ts so it can be unit-tested in isolation. Covers stop with source=pty + terminalId → TerminalManager.close, stop on a non-pty server → not-stoppable, openInBrowser with/without a url, and the registry's subscribe + snapshot semantics that the ws.ts subscribeDetectedServerEvents handler relies on. Web tests: - DetectedServersPanel.test.tsx: extracts a pure DetectedServersPanelView taking plain props (the Zustand-backed wrapper stays as the production entrypoint) and renders it via renderToStaticMarkup. Covers empty state, one row per server, and active-row styling. - DetectedServerRow.test.tsx: status-pill class table, hidden Open/Copy buttons when url is absent, and Stop button disabled for exited/crashed. - useDetectedServersSubscription hook: extracts the pure subscribe helper out of the useEffect closure in ChatView.tsx and tests the subscribe → unsubscribe → re-subscribe sequence as the active thread changes, plus the no-op paths when environmentId/threadId are missing.
Without the brand casts, tsc rejected the new useDetectedServersSubscription helper and its test on three lines (TS2345), which broke CI. The pattern matches the existing call sites in threadRoutes.ts that pass raw string URL params into scopeThreadRef.
Support multi-command PTY tracking and improve detected servers
Resolves package rename @s3tools/ -> @ryco/ alongside new DetectedServers service registrations.
- ws.ts: use Effect.runFork in detected-servers stream callback - DetectedServersIngress: only emit a single "confirmed" probe update - Registry: return defensive copies of DetectedServer state; isolate listener failures during publish fanout - CodexSessionRuntime: finalize tracked agent commands on session close - CursorAdapter: thread-scope tracker keys; end trackers when the first event is already terminal - DetectedServerRow: replace outer <button> wrapper with role=button div to avoid nested interactives - DetectedServersPanel: handle RPC/clipboard failures in action handlers - contracts: tighten patch schema to require positive port/pid - integration test: poll registry instead of fixed sleep - docs: align spec with ws.ts filename - rename remaining @s3tools/ imports to @ryco/ in detected-servers files
…m/sak0a/ryco into feature/auto-detect-agent-servers # Conflicts: # apps/server/src/detectedServers/Layers/DetectedServersIngress.ts # apps/server/src/provider/Layers/CursorAdapter.ts # apps/server/src/ws.ts # apps/web/src/components/detectedServers/DetectedServersPanel.tsx # packages/contracts/src/detectedServers.ts
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (1)
apps/server/src/detectedServers/Layers/DetectedServersIngress.ts (1)
16-23:⚠️ Potential issue | 🟠 Major | ⚡ Quick winBare
--inspectflags are not currently captured—apply the suggested fix.The
argvHasInspectfunction only matches--inspect=PORTforms (with explicit=PORT). Bare flags like--inspect,--inspect-brk, and--inspect-waitwithout an explicit port are silently ignored, even though they should default to port 9229 per Node.js behavior. This breaks debugger port detection for common use cases.The regex at line 19 should be updated to handle both forms:
Suggested fix
const argvHasInspect = (argv: ReadonlyArray<string>): number[] => { const out: number[] = []; for (const t of argv) { + if (/^--inspect(?:-brk|-wait)?$/.test(t)) { + out.push(9229); + continue; + } const m = t.match(/--inspect(?:-brk|-wait)?=(?:[^:]*:)?(\d+)/); if (m) out.push(Number.parseInt(m[1]!, 10)); } return out; };🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/server/src/detectedServers/Layers/DetectedServersIngress.ts` around lines 16 - 23, The argvHasInspect function currently only matches --inspect forms with an explicit "=PORT"; update its pattern and logic in argvHasInspect to also detect bare flags (--inspect, --inspect-brk, --inspect-wait) and treat them as port 9229 when no port is provided: modify the regex used in argvHasInspect to allow an optional "=(...)" capture and then if the capture is missing/empty push 9229, otherwise parse the captured port number as before (refer to argvHasInspect and the variable m in the loop).
🧹 Nitpick comments (1)
apps/server/src/detectedServers/DetectedServersIngress.test.ts (1)
76-77: 💤 Low valueAdd length assertion before accessing array element.
The test accesses
updated[0]!without first assertingupdated.toHaveLength(1). If the test fails with an empty array, the error message will be less clear.♻️ Clearer test structure
const updated = yield* registry.getCurrent("t-pty"); + expect(updated).toHaveLength(1); expect(updated[0]!.status === "candidate" || updated[0]!.status === "live").toBe(true); expect(updated[0]!.port).toBe(5173);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/server/src/detectedServers/DetectedServersIngress.test.ts` around lines 76 - 77, The test reads updated[0]! without asserting the array length, which can cause unclear failures; add an explicit assertion like expect(updated).toHaveLength(1) (or appropriate expected length) before accessing updated[0]!, then keep the existing assertions on updated[0]!.status and updated[0]!.port to ensure clear, early failure if the array is empty.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/server/src/detectedServers/Handlers.ts`:
- Around line 21-24: The handler currently swallows terminal close failures by
calling terminalManager.close(...).pipe(Effect.ignore({ log: true })) and then
returns { kind: "stopped" } which can report false success; remove the ignore
and instead let terminalManager.close(...) failures propagate (or explicitly map
them to an error case) so the handler only returns { kind: "stopped" } on
success; update the code around terminalManager.close, Effect.ignore, and the
return of { kind: "stopped" } to either await/return the effect directly or use
Effect.mapError to convert failures into a handler error path so failures are
not hidden.
- Around line 39-40: The call to open.openBrowser(server.url) is currently piped
to Effect.ignore({ log: true }) which swallows failures and causes the function
to always return { ok: true }; update the handler so it does not ignore runtime
failures: capture the effect result/exit of open.openBrowser (instead of piping
to Effect.ignore) and return { ok: false } when it fails (or rethrow the error)
and only return { ok: true } when the effect succeeds; locate the call to
open.openBrowser in Handlers.ts and replace the Effect.ignore usage with proper
error handling (e.g., inspect the Exit/Result or use Effect.map/Effect.catch to
convert failure into { ok: false }) so RPC/UI consumers get an accurate
success/failure response.
In `@apps/server/src/detectedServers/Layers/SocketProbe.Linux.ts`:
- Around line 34-50: parseProcTcpRows (and similarly parseProcTcp6Rows)
currently uses non-null assertions on parts[1], parts[3], parts[9] and will
throw if a line is malformed; update the parsing to defensively validate each
line before using indices: verify parts.length is sufficient, that parts[1]
contains a ":" split, and that numeric/hex fields parse successfully (e.g.,
portHex, inode), and skip or continue on malformed lines (optionally log or
collect a warning) instead of throwing; ensure you still map state via STATE_MAP
and convert host via hexToIpv4 only after these checks so malformed input cannot
crash the probe.
In `@apps/server/src/provider/acp/AcpDetectedServersTap.ts`:
- Around line 21-30: The consume method assumes growth is always an append;
update consume (and use lengthByKey and maybe a stored prefix snapshot if
needed) to verify append semantics by checking whether the new detail startsWith
the previous prefix before slicing—if detail.length > previousLength but detail
does not startWith the previous prefix (or detail.length == previousLength but
content differs), treat it as a non-prefix rewrite: update the stored length and
return the full detail (i.e., re-feed whole content) instead of returning null
or a corrupted suffix so callers receive a consistent full update when append
semantics are violated.
---
Duplicate comments:
In `@apps/server/src/detectedServers/Layers/DetectedServersIngress.ts`:
- Around line 16-23: The argvHasInspect function currently only matches
--inspect forms with an explicit "=PORT"; update its pattern and logic in
argvHasInspect to also detect bare flags (--inspect, --inspect-brk,
--inspect-wait) and treat them as port 9229 when no port is provided: modify the
regex used in argvHasInspect to allow an optional "=(...)" capture and then if
the capture is missing/empty push 9229, otherwise parse the captured port number
as before (refer to argvHasInspect and the variable m in the loop).
---
Nitpick comments:
In `@apps/server/src/detectedServers/DetectedServersIngress.test.ts`:
- Around line 76-77: The test reads updated[0]! without asserting the array
length, which can cause unclear failures; add an explicit assertion like
expect(updated).toHaveLength(1) (or appropriate expected length) before
accessing updated[0]!, then keep the existing assertions on updated[0]!.status
and updated[0]!.port to ensure clear, early failure if the array is empty.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: c547d4a6-41ee-494b-a41b-35d3351638d0
📒 Files selected for processing (33)
apps/server/integration/OrchestrationEngineHarness.integration.tsapps/server/integration/detectedServersCodex.integration.test.tsapps/server/integration/detectedServersPty.integration.test.tsapps/server/package.jsonapps/server/src/detectedServers/DetectedServersIngress.test.tsapps/server/src/detectedServers/Handlers.test.tsapps/server/src/detectedServers/Handlers.tsapps/server/src/detectedServers/Layers/ArgvHinter.tsapps/server/src/detectedServers/Layers/DetectedServersIngress.tsapps/server/src/detectedServers/Layers/LivenessHeartbeat.tsapps/server/src/detectedServers/Layers/Registry.tsapps/server/src/detectedServers/Layers/SocketProbe.Darwin.tsapps/server/src/detectedServers/Layers/SocketProbe.Linux.tsapps/server/src/detectedServers/Layers/SocketProbe.Windows.tsapps/server/src/detectedServers/Layers/StdoutSniffer.tsapps/server/src/detectedServers/LivenessHeartbeat.test.tsapps/server/src/detectedServers/PtyCommandTracker.test.tsapps/server/src/detectedServers/PtyInputLineBuffer.tsapps/server/src/detectedServers/Registry.test.tsapps/server/src/detectedServers/Services/DetectedServerRegistry.tsapps/server/src/provider/Drivers/CodexDriver.tsapps/server/src/provider/Drivers/CursorDriver.tsapps/server/src/provider/Layers/CodexAdapter.test.tsapps/server/src/provider/Layers/CodexAdapter.tsapps/server/src/provider/Layers/CodexSessionRuntime.tsapps/server/src/provider/Layers/CursorAdapter.test.tsapps/server/src/provider/Layers/CursorAdapter.tsapps/server/src/provider/Layers/ProviderInstanceRegistryLive.test.tsapps/server/src/provider/Layers/ProviderRegistry.test.tsapps/server/src/provider/acp/AcpDetectedServersTap.test.tsapps/server/src/provider/acp/AcpDetectedServersTap.tsapps/server/src/server.test.tsapps/server/src/server.ts
✅ Files skipped from review due to trivial changes (2)
- apps/server/src/detectedServers/LivenessHeartbeat.test.ts
- apps/server/package.json
🚧 Files skipped from review as they are similar to previous changes (22)
- apps/server/integration/OrchestrationEngineHarness.integration.ts
- apps/server/integration/detectedServersCodex.integration.test.ts
- apps/server/src/server.test.ts
- apps/server/integration/detectedServersPty.integration.test.ts
- apps/server/src/provider/Layers/CursorAdapter.ts
- apps/server/src/provider/Drivers/CodexDriver.ts
- apps/server/src/detectedServers/Registry.test.ts
- apps/server/src/provider/Layers/CodexAdapter.test.ts
- apps/server/src/provider/Drivers/CursorDriver.ts
- apps/server/src/detectedServers/Layers/LivenessHeartbeat.ts
- apps/server/src/provider/Layers/CodexAdapter.ts
- apps/server/src/provider/Layers/CodexSessionRuntime.ts
- apps/server/src/detectedServers/Layers/StdoutSniffer.ts
- apps/server/src/detectedServers/Layers/Registry.ts
- apps/server/src/detectedServers/Layers/SocketProbe.Darwin.ts
- apps/server/src/detectedServers/Services/DetectedServerRegistry.ts
- apps/server/src/provider/Layers/CursorAdapter.test.ts
- apps/server/src/detectedServers/Layers/ArgvHinter.ts
- apps/server/src/server.ts
- apps/server/src/detectedServers/Layers/SocketProbe.Windows.ts
- apps/server/src/provider/Layers/ProviderInstanceRegistryLive.test.ts
- apps/server/src/provider/Layers/ProviderRegistry.test.ts
| yield* terminalManager | ||
| .close({ threadId: server.threadId, terminalId: server.terminalId }) | ||
| .pipe(Effect.ignore({ log: true })); | ||
| return { kind: "stopped" } as const; |
There was a problem hiding this comment.
Don’t report stopped when terminal close fails.
Effect.ignore({ log: true }) swallows close failures, but the handler still returns { kind: "stopped" }. That can produce false success in the UI while the server is still running. Prefer propagating the error (or mapping it explicitly) instead of ignoring it.
Suggested fix
if (server.source === "pty" && server.terminalId) {
- yield* terminalManager
- .close({ threadId: server.threadId, terminalId: server.terminalId })
- .pipe(Effect.ignore({ log: true }));
+ yield* terminalManager.close({
+ threadId: server.threadId,
+ terminalId: server.terminalId,
+ });
return { kind: "stopped" } as const;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| yield* terminalManager | |
| .close({ threadId: server.threadId, terminalId: server.terminalId }) | |
| .pipe(Effect.ignore({ log: true })); | |
| return { kind: "stopped" } as const; | |
| yield* terminalManager.close({ | |
| threadId: server.threadId, | |
| terminalId: server.terminalId, | |
| }); | |
| return { kind: "stopped" } as const; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/server/src/detectedServers/Handlers.ts` around lines 21 - 24, The
handler currently swallows terminal close failures by calling
terminalManager.close(...).pipe(Effect.ignore({ log: true })) and then returns {
kind: "stopped" } which can report false success; remove the ignore and instead
let terminalManager.close(...) failures propagate (or explicitly map them to an
error case) so the handler only returns { kind: "stopped" } on success; update
the code around terminalManager.close, Effect.ignore, and the return of { kind:
"stopped" } to either await/return the effect directly or use Effect.mapError to
convert failures into a handler error path so failures are not hidden.
| yield* open.openBrowser(server.url).pipe(Effect.ignore({ log: true })); | ||
| return { ok: true } as const; |
There was a problem hiding this comment.
Avoid returning { ok: true } if browser open fails.
This currently ignores runtime failures and still reports success, which is misleading for RPC/UI consumers. Return ok: false on failure (or propagate the error).
Suggested fix
- yield* open.openBrowser(server.url).pipe(Effect.ignore({ log: true }));
- return { ok: true } as const;
+ const opened = yield* open
+ .openBrowser(server.url)
+ .pipe(Effect.as(true as const), Effect.catchAll(() => Effect.succeed(false as const)));
+ return { ok: opened } as const;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| yield* open.openBrowser(server.url).pipe(Effect.ignore({ log: true })); | |
| return { ok: true } as const; | |
| const opened = yield* open | |
| .openBrowser(server.url) | |
| .pipe(Effect.as(true as const), Effect.catchAll(() => Effect.succeed(false as const))); | |
| return { ok: opened } as const; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/server/src/detectedServers/Handlers.ts` around lines 39 - 40, The call
to open.openBrowser(server.url) is currently piped to Effect.ignore({ log: true
}) which swallows failures and causes the function to always return { ok: true
}; update the handler so it does not ignore runtime failures: capture the effect
result/exit of open.openBrowser (instead of piping to Effect.ignore) and return
{ ok: false } when it fails (or rethrow the error) and only return { ok: true }
when the effect succeeds; locate the call to open.openBrowser in Handlers.ts and
replace the Effect.ignore usage with proper error handling (e.g., inspect the
Exit/Result or use Effect.map/Effect.catch to convert failure into { ok: false
}) so RPC/UI consumers get an accurate success/failure response.
| export const parseProcTcpRows = (text: string): ProcTcpRow[] => { | ||
| const lines = text | ||
| .split("\n") | ||
| .slice(1) | ||
| .filter((l) => l.trim().length > 0); | ||
| return lines.map((line) => { | ||
| const parts = line.trim().split(/\s+/); | ||
| const [hostHex, portHex] = parts[1]!.split(":"); | ||
| const state = STATE_MAP[parts[3]!] ?? parts[3]!; | ||
| return { | ||
| inode: Number.parseInt(parts[9]!, 10), | ||
| port: Number.parseInt(portHex!, 16), | ||
| host: hexToIpv4(hostHex!), | ||
| state, | ||
| }; | ||
| }); | ||
| }; |
There was a problem hiding this comment.
Add defensive bounds checking in /proc parsers.
Both parseProcTcpRows and parseProcTcp6Rows use non-null assertions when accessing split results (e.g., parts[1]!, parts[3]!, parts[9]!) without verifying array length. If /proc/net/tcp or /proc/net/tcp6 format is unexpectedly malformed, these will throw runtime exceptions and crash the probe.
🛡️ Suggested defensive parsing
export const parseProcTcpRows = (text: string): ProcTcpRow[] => {
const lines = text
.split("\n")
.slice(1)
.filter((l) => l.trim().length > 0);
- return lines.map((line) => {
+ return lines.flatMap((line) => {
const parts = line.trim().split(/\s+/);
+ if (parts.length < 10) return [];
const [hostHex, portHex] = parts[1]!.split(":");
+ if (!hostHex || !portHex) return [];
const state = STATE_MAP[parts[3]!] ?? parts[3]!;
return {
inode: Number.parseInt(parts[9]!, 10),
port: Number.parseInt(portHex!, 16),
host: hexToIpv4(hostHex!),
state,
};
});
};Apply the same pattern to parseProcTcp6Rows.
Also applies to: 52-68
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/server/src/detectedServers/Layers/SocketProbe.Linux.ts` around lines 34
- 50, parseProcTcpRows (and similarly parseProcTcp6Rows) currently uses non-null
assertions on parts[1], parts[3], parts[9] and will throw if a line is
malformed; update the parsing to defensively validate each line before using
indices: verify parts.length is sufficient, that parts[1] contains a ":" split,
and that numeric/hex fields parse successfully (e.g., portHex, inode), and skip
or continue on malformed lines (optionally log or collect a warning) instead of
throwing; ensure you still map state via STATE_MAP and convert host via
hexToIpv4 only after these checks so malformed input cannot crash the probe.
| consume(key: string, detail: string): string | null { | ||
| const previousLength = this.lengthByKey.get(key) ?? 0; | ||
| if (detail.length < previousLength) { | ||
| this.lengthByKey.set(key, detail.length); | ||
| return detail; | ||
| } | ||
| if (detail.length > previousLength) { | ||
| this.lengthByKey.set(key, detail.length); | ||
| return detail.slice(previousLength); | ||
| } |
There was a problem hiding this comment.
Handle non-prefix rewrites in cumulative updates.
consume assumes every growth is a strict append. If detail is rewritten (same length or changed prefix), this can incorrectly return null or emit a corrupted suffix. Add a prefix check and fall back to full re-feed when append semantics are violated.
Proposed fix
export class AcpDetailSuffixDedup {
private readonly lengthByKey = new Map<string, number>();
+ private readonly lastDetailByKey = new Map<string, string>();
@@
consume(key: string, detail: string): string | null {
+ const previousDetail = this.lastDetailByKey.get(key) ?? "";
const previousLength = this.lengthByKey.get(key) ?? 0;
+
+ // unchanged
+ if (detail === previousDetail) return null;
+
+ // strict append
+ if (detail.length >= previousLength && detail.startsWith(previousDetail)) {
+ this.lengthByKey.set(key, detail.length);
+ this.lastDetailByKey.set(key, detail);
+ return detail.slice(previousLength);
+ }
+
+ // defensive fallback for shrink or rewrite
+ this.lengthByKey.set(key, detail.length);
+ this.lastDetailByKey.set(key, detail);
+ return detail;
- if (detail.length < previousLength) {
- this.lengthByKey.set(key, detail.length);
- return detail;
- }
- if (detail.length > previousLength) {
- this.lengthByKey.set(key, detail.length);
- return detail.slice(previousLength);
- }
- return null;
}
@@
reset(key: string): void {
this.lengthByKey.delete(key);
+ this.lastDetailByKey.delete(key);
}
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/server/src/provider/acp/AcpDetectedServersTap.ts` around lines 21 - 30,
The consume method assumes growth is always an append; update consume (and use
lengthByKey and maybe a stored prefix snapshot if needed) to verify append
semantics by checking whether the new detail startsWith the previous prefix
before slicing—if detail.length > previousLength but detail does not startWith
the previous prefix (or detail.length == previousLength but content differs),
treat it as a non-prefix rewrite: update the stored length and return the full
detail (i.e., re-feed whole content) instead of returning null or a corrupted
suffix so callers receive a consistent full update when append semantics are
violated.
These files were added by the upstream PR #63 merge and were not caught by the earlier rename sweep, breaking apps/web typecheck on CI.
The selector returned a fresh array on every call which breaks Zustand reference equality and triggers a render -> select -> render loop. Switch to selecting the underlying Map and deriving the array via useMemo, matching the pattern in DetectedServersPanel.tsx.
When a dev server briefly binds one port (e.g. 3000) during startup and then settles on another (e.g. 5733), the probe could see both ports listed in lsof for one iteration. The previous logic picked candidates[0] and, on heartbeat failure, latched the entry into 'confirmed' with the wrong port and never updated. Now: iterate candidates (sniffed port first when known) and pick the one whose heartbeat succeeds. If none respond, fall back to the preferred candidate as 'confirmed' but allow the port to change across iterations until something actually responds.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (5)
apps/web/src/components/detectedServers/DetectedServersPanel.test.tsx (2)
69-74: ⚡ Quick winRemove placeholder test or implement proper handler verification.
This test verifies that
renderToStaticMarkupdoesn't call handlers—a documented behavior of the API. It adds no coverage value and the title acknowledges it's a placeholder.Either remove it or use
@testing-library/reactwithrender()andfireEventto verify the actualonStophandler wiring.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/components/detectedServers/DetectedServersPanel.test.tsx` around lines 69 - 74, The placeholder test "vi noop sanity (placeholder for future onStop handler verification)" provides no value—either delete this test or replace it with a real interaction test: use `@testing-library/react`'s render() instead of renderToStaticMarkup via renderView, locate the stop control in the rendered output (e.g., by role/text), call fireEvent.click or userEvent.click, and assert that the onStop mock (created by vi.fn()) was called; reference the existing helpers make, renderView and the onStop mock when making the change.
60-67: ⚡ Quick winConsider more robust assertions for styling verification.
The test counts
bg-accentoccurrences in the static HTML string, which tightly couples the test to Tailwind implementation details. This will break if class names change or if the CSS approach is refactored (e.g., CSS-in-JS, CSS modules).Consider using
@testing-library/reactwithrender()instead ofrenderToStaticMarkupto verify computed styles or DOM structure more robustly.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/components/detectedServers/DetectedServersPanel.test.tsx` around lines 60 - 67, The test should stop counting occurrences in the HTML string and instead render the component with `@testing-library/react` and assert DOM/classes directly: update renderView to use render() (from `@testing-library/react`) rather than renderToStaticMarkup, render the rows created by make({ id }) and then query the active row (e.g., getByText or a data-testid on the row) and assert the element hasClass 'bg-accent' (use expect(element).toHaveClass('bg-accent')) and that a non-active row does not have the bare class; this makes the test resilient to changes in static markup or CSS build details.apps/web/src/components/detectedServers/DetectedServerRow.test.tsx (3)
60-61: 💤 Low valueRegex assumes attribute ordering in static markup.
The regex
/aria-label="Stop"[^>]* disabled=""/requiresaria-labelto appear beforedisabledin the HTML. WhilerenderToStaticMarkupcurrently has consistent attribute ordering, this assumption is fragile.Consider a more robust approach like:
- Checking for both attributes independently
- Using a lightweight HTML parser
- Switching to
@testing-library/reactwhich providestoBeDisabled()matcher🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/components/detectedServers/DetectedServerRow.test.tsx` around lines 60 - 61, The helper stopBtnDisabled currently uses a fragile regex that assumes aria-label appears before disabled; update the test to locate the element and check the disabled state instead of relying on attribute order — replace the stopBtnDisabled(regex) approach by rendering the component (renderToStaticMarkup or better, use `@testing-library/react`'s render) and query the element with aria-label "Stop" (e.g., getByLabelText/queryByLabelText) then assert its disabled property or use the toBeDisabled() matcher; alternatively, if you must work with static markup, parse markup to find the element and check for the presence of the disabled attribute independently of attribute order (modify the stopBtnDisabled function to test for both aria-label="Stop" and disabled separately).
75-82: ⚡ Quick winConsider more robust assertions for styling verification.
Counting
bg-accentoccurrences tightly couples the test to Tailwind class implementation. This mirrors the same brittleness inDetectedServersPanel.test.tsx. Consider using@testing-library/reactwithrender()to verify computed styles or DOM structure instead.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/components/detectedServers/DetectedServerRow.test.tsx` around lines 75 - 82, The test in DetectedServerRow.test.tsx currently uses a brittle regex count on renderRow(...) output; update the test to render the component with `@testing-library/react` (or make renderRow return the render container) and assert against the DOM/classList instead of string matches: locate the renderRow helper and make it return the render() result or container, then in the test use screen/getByRole or container.querySelector to find the row/button element produced by make(...) and assert element.classList.contains('bg-accent') for the active case and not for the inactive case (or assert exact classList length if you need to verify order), replacing the regex expectations with these DOM-based assertions.
32-44: ⚡ Quick winConsider testing visual behavior rather than Tailwind class names.
The test directly asserts Tailwind class strings (e.g.,
bg-blue-500/20), coupling tests to implementation details. If styling moves to CSS-in-JS, CSS modules, or class names change, these tests will break despite correct visual output.For a more robust approach, consider using
@testing-library/reactto verify computed styles or semantic attributes.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/components/detectedServers/DetectedServerRow.test.tsx` around lines 32 - 44, The test DetectedServerRow.test.tsx is brittle because it asserts Tailwind class strings; update the it.each block that calls renderRow(make({ status })) to assert visual/semantic output instead of class names: locate DetectedServerRow (and helper renderRow/make) and change the assertions to query the status pill element (by role/text/data-testid) and verify its computed style (e.g., backgroundColor via window.getComputedStyle) or a semantic attribute (aria-label or title) that reflects the status; alternatively add a stable data-testid on the pill in DetectedServerRow and assert computed styles or status text so tests don't depend on specific Tailwind class names.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/web/src/components/detectedServers/DetectedServerRow.test.tsx`:
- Around line 13-14: The tests currently bypass type safety by double-casting
Date objects for the DetectedServer properties startedAt and lastSeenAt; replace
the "new Date() as unknown as DetectedServer['startedAt']" and similar with
properly typed serialized date strings (e.g., use new Date().toISOString() or a
literal ISO string) so the values match DetectedServer's expected type rather
than forcing a Date through a double cast; mirror the same change made in
DetectedServersPanel.test.tsx and update any assertions that expect ISO strings
if necessary.
In `@apps/web/src/components/detectedServers/DetectedServersPanel.test.tsx`:
- Around line 13-14: The test uses a double cast to force new Date() into
DetectedServer["startedAt"] and ["lastSeenAt"]; instead, inspect the
DetectedServer type in
apps/web/src/components/detectedServers/DetectedServersPanel.test.tsx and
replace the unsafe "as unknown as" casts with correctly typed values: if the
fields are Date, assign new Date() directly (or new Date(timestamp) for
deterministic tests); if they are serialized strings, assign new
Date().toISOString() (or a fixed ISO string) for both startedAt and lastSeenAt
to preserve type safety and avoid masking type mismatches.
---
Nitpick comments:
In `@apps/web/src/components/detectedServers/DetectedServerRow.test.tsx`:
- Around line 60-61: The helper stopBtnDisabled currently uses a fragile regex
that assumes aria-label appears before disabled; update the test to locate the
element and check the disabled state instead of relying on attribute order —
replace the stopBtnDisabled(regex) approach by rendering the component
(renderToStaticMarkup or better, use `@testing-library/react`'s render) and query
the element with aria-label "Stop" (e.g., getByLabelText/queryByLabelText) then
assert its disabled property or use the toBeDisabled() matcher; alternatively,
if you must work with static markup, parse markup to find the element and check
for the presence of the disabled attribute independently of attribute order
(modify the stopBtnDisabled function to test for both aria-label="Stop" and
disabled separately).
- Around line 75-82: The test in DetectedServerRow.test.tsx currently uses a
brittle regex count on renderRow(...) output; update the test to render the
component with `@testing-library/react` (or make renderRow return the render
container) and assert against the DOM/classList instead of string matches:
locate the renderRow helper and make it return the render() result or container,
then in the test use screen/getByRole or container.querySelector to find the
row/button element produced by make(...) and assert
element.classList.contains('bg-accent') for the active case and not for the
inactive case (or assert exact classList length if you need to verify order),
replacing the regex expectations with these DOM-based assertions.
- Around line 32-44: The test DetectedServerRow.test.tsx is brittle because it
asserts Tailwind class strings; update the it.each block that calls
renderRow(make({ status })) to assert visual/semantic output instead of class
names: locate DetectedServerRow (and helper renderRow/make) and change the
assertions to query the status pill element (by role/text/data-testid) and
verify its computed style (e.g., backgroundColor via window.getComputedStyle) or
a semantic attribute (aria-label or title) that reflects the status;
alternatively add a stable data-testid on the pill in DetectedServerRow and
assert computed styles or status text so tests don't depend on specific Tailwind
class names.
In `@apps/web/src/components/detectedServers/DetectedServersPanel.test.tsx`:
- Around line 69-74: The placeholder test "vi noop sanity (placeholder for
future onStop handler verification)" provides no value—either delete this test
or replace it with a real interaction test: use `@testing-library/react`'s
render() instead of renderToStaticMarkup via renderView, locate the stop control
in the rendered output (e.g., by role/text), call fireEvent.click or
userEvent.click, and assert that the onStop mock (created by vi.fn()) was
called; reference the existing helpers make, renderView and the onStop mock when
making the change.
- Around line 60-67: The test should stop counting occurrences in the HTML
string and instead render the component with `@testing-library/react` and assert
DOM/classes directly: update renderView to use render() (from
`@testing-library/react`) rather than renderToStaticMarkup, render the rows
created by make({ id }) and then query the active row (e.g., getByText or a
data-testid on the row) and assert the element hasClass 'bg-accent' (use
expect(element).toHaveClass('bg-accent')) and that a non-active row does not
have the bare class; this makes the test resilient to changes in static markup
or CSS build details.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: f2b5c980-004e-4b60-acea-370ef20b84ce
⛔ Files ignored due to path filters (1)
.claude/scheduled_tasks.lockis excluded by!**/*.lock
📒 Files selected for processing (9)
apps/server/integration/detectedServersCodex.integration.test.tsapps/server/src/detectedServers/DetectedServersIngress.test.tsapps/server/src/detectedServers/Handlers.test.tsapps/server/src/detectedServers/Layers/DetectedServersIngress.tsapps/web/src/components/BranchToolbar.tsxapps/web/src/components/detectedServers/DetectedServerRow.test.tsxapps/web/src/components/detectedServers/DetectedServersPanel.test.tsxapps/web/src/hooks/useDetectedServersSubscription.test.tsapps/web/src/hooks/useDetectedServersSubscription.ts
🚧 Files skipped from review as they are similar to previous changes (4)
- apps/server/integration/detectedServersCodex.integration.test.ts
- apps/web/src/components/BranchToolbar.tsx
- apps/server/src/detectedServers/Handlers.test.ts
- apps/server/src/detectedServers/Layers/DetectedServersIngress.ts
| startedAt: new Date() as unknown as DetectedServer["startedAt"], | ||
| lastSeenAt: new Date() as unknown as DetectedServer["lastSeenAt"], |
There was a problem hiding this comment.
Replace double type casting with properly typed date values.
Same issue as in DetectedServersPanel.test.tsx: the as unknown as double casting bypasses type safety. Use proper date serialization (e.g., .toISOString()) instead of forcing Date objects into fields that likely expect strings.
🔧 Proposed fix
- startedAt: new Date() as unknown as DetectedServer["startedAt"],
- lastSeenAt: new Date() as unknown as DetectedServer["lastSeenAt"],
+ startedAt: new Date().toISOString() as DetectedServer["startedAt"],
+ lastSeenAt: new Date().toISOString() as DetectedServer["lastSeenAt"],📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| startedAt: new Date() as unknown as DetectedServer["startedAt"], | |
| lastSeenAt: new Date() as unknown as DetectedServer["lastSeenAt"], | |
| startedAt: new Date().toISOString() as DetectedServer["startedAt"], | |
| lastSeenAt: new Date().toISOString() as DetectedServer["lastSeenAt"], |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web/src/components/detectedServers/DetectedServerRow.test.tsx` around
lines 13 - 14, The tests currently bypass type safety by double-casting Date
objects for the DetectedServer properties startedAt and lastSeenAt; replace the
"new Date() as unknown as DetectedServer['startedAt']" and similar with properly
typed serialized date strings (e.g., use new Date().toISOString() or a literal
ISO string) so the values match DetectedServer's expected type rather than
forcing a Date through a double cast; mirror the same change made in
DetectedServersPanel.test.tsx and update any assertions that expect ISO strings
if necessary.
| startedAt: new Date() as unknown as DetectedServer["startedAt"], | ||
| lastSeenAt: new Date() as unknown as DetectedServer["lastSeenAt"], |
There was a problem hiding this comment.
Replace double type casting with properly typed date values.
The as unknown as double casting bypasses TypeScript's type safety and suggests DetectedServer["startedAt"] and DetectedServer["lastSeenAt"] are not Date objects (likely serialized strings). This could mask type mismatches and cause issues if the contract expects a specific string format.
🔧 Proposed fix to use proper date serialization
const make = (overrides: Partial<DetectedServer>): DetectedServer => ({
id: "s",
threadId: "t",
source: "pty",
framework: "vite",
status: "live",
url: "http://localhost:5173",
- startedAt: new Date() as unknown as DetectedServer["startedAt"],
- lastSeenAt: new Date() as unknown as DetectedServer["lastSeenAt"],
+ startedAt: new Date().toISOString() as DetectedServer["startedAt"],
+ lastSeenAt: new Date().toISOString() as DetectedServer["lastSeenAt"],
...overrides,
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| startedAt: new Date() as unknown as DetectedServer["startedAt"], | |
| lastSeenAt: new Date() as unknown as DetectedServer["lastSeenAt"], | |
| const make = (overrides: Partial<DetectedServer>): DetectedServer => ({ | |
| id: "s", | |
| threadId: "t", | |
| source: "pty", | |
| framework: "vite", | |
| status: "live", | |
| url: "http://localhost:5173", | |
| startedAt: new Date().toISOString() as DetectedServer["startedAt"], | |
| lastSeenAt: new Date().toISOString() as DetectedServer["lastSeenAt"], | |
| ...overrides, | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web/src/components/detectedServers/DetectedServersPanel.test.tsx` around
lines 13 - 14, The test uses a double cast to force new Date() into
DetectedServer["startedAt"] and ["lastSeenAt"]; instead, inspect the
DetectedServer type in
apps/web/src/components/detectedServers/DetectedServersPanel.test.tsx and
replace the unsafe "as unknown as" casts with correctly typed values: if the
fields are Date, assign new Date() directly (or new Date(timestamp) for
deterministic tests); if they are serialized strings, assign new
Date().toISOString() (or a fixed ISO string) for both startedAt and lastSeenAt
to preserve type safety and avoid masking type mismatches.
Summary
s3naming.Testing
ArgvHinter,Registry,StdoutSniffer, OS socket probes, and detected-server integration paths.Summary by CodeRabbit
New Features
Tests
Documentation